# 正文

之前的小节中我们已经对 css做过处理,将所有的css打包到一个文件内,然后作为资源link和组件的 html内容一起直出到客户端。

image-20210214220307869

# 发现问题

上面的方式很简单也很有效。

但存在一些问题,当项目体量上来后css代码量剧增,导致最终打包的css文件会过大,另外只要一个页面的css内容产生变化,就会导致此文件的缓存生效,用户端就需要重新下载,最终会对体验和性能造成影响。

# 优化思路

如何来解决这个问题呢?

前面的小节中我们对js 业务代码进行了优化,使用了路由拆分,按需加载,只加载基础库和当前页面的代码,大大缩减了所需下载的资源体积。

所以我们也可以使用按需加载的方式对该问题进行优化处理。

# 实现分析

如何实现呢?

相信大家都用过style-loader,该库的作用是将模块引入的 css,在客户端渲染的时候以内联的形式动态插入到head内。

image-20210214220322543

上图便是我们在单页应用开发中的必然产物。

那么插入到DOM时的css内容从哪里获得的呢?

这里就需要说到css-loader了,它的存在是很强大的,js模块内导入的css文件能够被处理,全仰仗该库的作用。

该库会把css代码转换成js代码或者css字符串,最终和js模块代码打包在一起,之后便能够作为js代码的一部分加载到客户端,然后style-loader便会简单粗暴的使用DOM操作将css中的样式插入head内。

那上面这些内容和我们的 css按需加载有什么关系呢?

我们可以得到一些信息,css-loader可以让我们得到导入的css文件的内容,如果我们得到了这些信息就可以在服务端直出组件的时候将css代码一同直出。

当客户端接管页面后,后续的访问就是单页应用了,此时css就应该是由客户端代码动态插入到head标签内。

但是上面介绍的style-loader就无法胜任了,它只能运行在客户端,在服务端就无法愉快的玩耍了。

所以我们要使用一种同构的方式来处理,让双端都可以运行。

# isomorphic-style-loader

该库没有像style-loader那样直接进行DOM操作,而是导出了一些辅助方法,让用户依据实际情况来调用不同的方法。

可以参考下面部分源码来理解下

//用于获得模块信息和 样式内容
exports._getContent = function() { return content; };
//获得 css 内容
exports._getCss = function() { return '' + css; };
//执行 dom 操作,将 css 动态插入到head 内
exports._insertCss = function(options) { return insertCss(content, options) };

可以先看下官方的说明,里面也包含了很多参考实例

github.com/kriasoft/is… (opens new window)

# 具体实现

现在我们已经了解了css同构直出的原理,接下来进行一步一步的实现。

# 从开发环境开始,首先调整 webpack 配置

之前我们是使用插件mini-css-extract-plugincss全部提取到一个文件内,现在这个插件就不需要使用了,替换为下面的配置。

客户端配置
// webpack/webpack.dev.config.js

//...
  {
                test: /\.(sa|sc|c)ss$/,
                use: ['isomorphic-style-loader',
                    {
                        loader: "css-loader",
                        options: {
                            importLoaders: 2
                        }
                    }, 'postcss-loader', 'sass-loader'
                ]
            }
//...
服务端配置

同时服务端webpack.server.config.js的配置和上面客户端的配置保持一致即可。

# 页面组件的调整

// ./src/client/pages/index/index.js

//导入 css
import css from  './index.scss';
//导入高阶组件,用于同构处理 css
import withStyles from 'isomorphic-style-loader/withStyles'

//组件代码 略...


export default withStyles(css)(PageContainer(Index));

# 客户端渲染入口的调整

//定义css处理逻辑,实现将 css 动态插入到`head`内

  const insertCss = (...styles) => {
                const removeCss = styles.map(style => style._insertCss());//客户端执行,插入style
                return () => removeCss.forEach(dispose => dispose());//组件卸载时 移除当前的 style 标签
        }


//导入内置的 context 组件,用于将上面的方法传递给子组件
import StyleContext from 'isomorphic-style-loader/StyleContext';

ReactDom.hydrate(<BrowserRouter>
    <StyleContext.Provider value={{ insertCss }}>
        <App routeList={routeList}/>
    </StyleContext.Provider>

</BrowserRouter>,document.getElementById('root'));
//...

# 服务端 ssr中间件调整

基本上和客户端的渲染部分差不多,只是服务端只需要收集到所有组件的css样式内容。

//定义存储组件 css 的变量
const css = new Set() // CSS for all rendered React components

//定义收集 css 的方法.此方法会在`withStyles`高阶组件内获得,然后执行该方法,完成 css 内容搜集。
const insertCss = (...styles) => styles.forEach(style => css.add(style._getContent()));

const html = renderToString(<StaticRouter location={path} context={context}>
<StyleContext.Provider value={{ insertCss }} >
    <App routeList={staticRoutesList}></App></StyleContext.Provider>
</StaticRouter>);

//...

# 配置基本完成,但存在问题

我们先来看下效果。

本地启动服务并运行,查看网页源代码的确能看到css直出到了页面。

image-20210214220404500

但是通过审查元素会发现问题,客户端也执行了插入,相当于是两份相同的 css

image-20210214220418139

正常情况下应该是服务端直出了css内容,客户端在插入前需要判断是否可以插入。

根据什么来判断呢?

image-20210214220439745

上面截图中能看到style标签上都有id的属性,所以关键就在这里,猜想肯定是通过id来判断。

//执行 dom 操作,将 css 动态插入到head 内
exports._insertCss = function(options) { return insertCss(content, options) };

_insertCss方法的内部逻辑也是通过 id来判断的。

下面是关键代码,一看便知。

// https://github.com/kriasoft/isomorphic-style-loader/blob/master/src/insertCss.js
//...

//根据 id 获取对应的节点
 let elem = document.getElementById(id)
    let create = false

    if (!elem) {//如果节点不存在 才会执行插入
      create = true

      elem = document.createElement('style')
      elem.setAttribute('type', 'text/css')
      elem.id = id

      if (media) {
        elem.setAttribute('media', media)
      }
    }

//...
如何给 style 标签 增加 id呢?
id 的取值又是什么,又如何取值呢?

其实isomorphic-style-loader已为我们提供,只是有时候需要多尝试下。

//用于获得模块信息 和 样式内容
exports._getContent = function() { return content; };

该方法会返回当前 css模块的id和样式信息。

在上面几张图中能看到id的取值是很长的字符串。之所以这么长,是因为在development环境中id值默认为模块的相对路径地址。

# 设置style标签 id

根据上面的分析,我们来对react ssr中间件做下调整。

    const css = new Set() ;
    - React components
    const insertCss = (...styles) => styles.forEach(style => css.add(style.——getCss()));

    + React components
    const insertCss = (...styles) => styles.forEach(style => css.add(style._getContent()));//该方法会获得css id 值

增加转换逻辑,在直出时可以带上style标签和id属性。

    const styles = [];
    [...css].forEach(item => {
        let [mid, content] = item[0];
        styles.push(`<style id="s${mid}-0">${content}</style>`)
    });
    //...

直出部分

<head>
    <meta charset="UTF-8">
    <title>${tdk.title}</title>
    <meta name="keywords" content="${tdk.keywords}" />
    <meta name="description" content="${tdk.description}" />
    ${styles.join('')}
</head>

# 生产环境处理

经过上一步的处理,目前已经不会重复插入style了。

image-20210214220457978

开发环境是 ok 了,不过生产环境中,仍然有坑。

继续往下看。

生产环境也主要是调整下webpack.prod.config.js配置,移除mini-css-extract-plugin的使用,调整 下scss相关loader配置即可。

   {
        test: /\.(sa|sc|c)ss$/,
        use: ['isomorphic-style-loader',
                {
                    loader: "css-loader",
                    options: {
                            importLoaders: 2
                        }
                }, 'postcss-loader', 'sass-loader'
            ]
    }

构建后,并启动生产环境服务。

image-20210214220514712

从上图中可以看出,在生产环境style标签的id不再是模块的相对路径,而变成了数字,比如s19-0

其中的s为前缀,后面的-0其实没用,永远都是-0,源码中本身可以删除这个逻辑。

问题出现了,当我们审查元素的时候发现style标签增多了,又出现了重复的插入,客户端排重失败。

image-20210214220529799

原因是:客户端的模块 id和服务器的模块id值不同。

为什么不同呢 ?

因为环境问题,打包的目标平台不同,所以node浏览器的打包内容也不同,所以就会导致模块的id值不同。

诶?可是在开发环境采用的是模块的路径是相同的,这个是肯定的。

# HashedModuleIdsPlugin 解决模块 id 不稳定问题

该插件会根据模块的相对路径生成一个四位数的hash作为模块id, 主要用于生产环境。

ps:服务端打包配置也需要配置此插件

new webpack.HashedModuleIdsPlugin({
  // Options...
})

ok,直接上插件。

// ./webpack/webpack.prod.config.js

  plugins: [
        new webpack.HashedModuleIdsPlugin(),
    //...
    ]

重新启动服务后,得到了我们期望的结果。

image-20210214220545056

# 但是最后还有个 bug

这个问题很难发现,隐藏的比较深。

我在验证的过程中发现了style标签内容会被替换,经过一番折腾验证了这个问题。

然后经过研究和排查,最终确定这该同构库的一个 bug

insertCss.js 文件

image-20210214220606381

以上代码中,id排重验证没有问题,到后面,也就是我标注的地方,判断是有问题的。

但我没理解为什么加这个判断,干掉以后就正常了。

所以也顺便给官方提了一个 pr (opens new window)

# 小结

本节我们再次对css代码进行了一次优化,采用的是同构直出的方式实现了css的按需加载,减少了请求次数,解决了单一文件的弊端。

另外也大致的了解了style-loader,css-loader以及isomorphic-style-loader的原理。

本节完整代码已上传

github.com/Bigerfe/koa… (opens new window)

阅读全文